/**
 * Copyright (C) 2008 Christopher Allen Ogden
 *
 *     This file is part of Odysi.
 *
 *  Odysi is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Odysi is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Odysi.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <sstream>
#include <stdexcept>
#include <vector>
#include "client.h"
#include "serverLua.h"
#include "luaAux.h"
#include "map.h"
#include "character.h"

namespace {
    static const std::string errorSendPacket("sendPacket: Invalid client.");
    static const std::string errorAddCharacterNoMap("addCharacter: Could not get map.");
    static const std::string errorAddCharacterNoChr("addCharacter: Could not make character.");
    typedef std::vector<Client> ClientList;

    /**
     * Send a packet to all clients.
     *
     * @param L     Lua state.
     * @param p     Packet.
     */
    void sendPacketToAll(lua_State *L, const Packet &p)
    {
        ClientList * clients = reinterpret_cast<ClientList *>(Lua::getRegisteredPointer(L, LUA_SR_CLIENTS));


        for (ClientList::iterator iter = clients->begin();
                iter != clients->end(); iter++) {
            iter->send(p);
        }
    }

    /**
     * Get a client by socket.
     *
     * @param L     The lua state.
     * @param sock  The socket.
     * @return      A pointer to the client.
     */
    Client *getClient(lua_State *L, int sock)
    {
        ClientList * clients = reinterpret_cast<ClientList *>(Lua::getRegisteredPointer(L, LUA_SR_CLIENTS));

        for (ClientList::iterator iter = clients->begin();
                iter != clients->end(); iter++) {
            if (iter->getSocket() == sock) return &*iter;
        }

        return NULL;
    }

    /**
     * Send a packet.
     *
     * @param L Lua state.
     * @param p Packet to send.
     */
    void sendPacket(lua_State *L, const Packet &p)
    {
        Client *c(getClient(L, lua_tonumber(L, 1)));
        if (c) {
            c->send(p);
        } else {
            throw std::runtime_error(errorSendPacket);
        }
    }

    /**
     * Associate a character ID with a client.
     *
     * @param L Lua state.
     */
    int setClientCharID(lua_State *L)
    {
        Client *c(getClient(L, lua_tonumber(L, 1)));
        if (c) {
            c->setCharID(lua_tonumber(L, 2));
        }
        return 0;
    }

    /**
     * Get associated character ID from a client.
     *
     * @param L Lua state.
     */
    int getClientCharID(lua_State *L)
    {
        Client *c(getClient(L, lua_tonumber(L, 1)));
        if (c) {
            lua_pushnumber(L, c->getCharID());
        } else {
            lua_pushnumber(L, 0);
        }
        return 1;
    }

    /**
     * Send login OK packet.
     */
    int sendLoginOKPacket(lua_State *L)
    {
        sendPacket(L, genPacket::loginOK());
        return 0;
    }

    /**
     * Send login fail packet.
     */
    int sendLoginFailPacket(lua_State *L)
    {
        sendPacket(L, genPacket::loginFail());
        return 0;
    }

    /**
     * Adds a character.
     */
    int addCharacter(lua_State *L)
    {
        sendPacketToAll(L, genPacket::addCharacter(luaL_checknumber(L, 1),
                            luaL_checknumber(L, 2),
                            luaL_checknumber(L, 3),
                            luaL_checknumber(L, 4),
                            luaL_checknumber(L, 5),
                            luaL_checknumber(L, 6),
                            luaL_checknumber(L, 7),
                            luaL_checknumber(L, 8),
                            luaL_checknumber(L, 9)));

        Map *map = reinterpret_cast<Map *>(Lua::getRegisteredPointer(L, LUA_SR_MAP));

        if (!map) throw std::runtime_error(errorAddCharacterNoMap);

        Character *chr = new Character;
        chr->setX(luaL_checknumber(L, 1));
        chr->setY(luaL_checknumber(L, 2));
        chr->setZ(luaL_checknumber(L, 3));
        chr->setMobID(luaL_checknumber(L, 4));
        chr->setID(luaL_checknumber(L, 5));
        chr->setMax(map->getWidth() - 1, map->getHeight() - 1);
        chr->setRed(luaL_checknumber(L, 6));
        chr->setGreen(luaL_checknumber(L, 7));
        chr->setBlue(luaL_checknumber(L, 8));
        chr->setAlpha(luaL_checknumber(L, 9));

        if (!chr) throw std::runtime_error(errorAddCharacterNoChr);

        map->addObject(chr);
        chr->setSpeed(CHARACTER_SPEED);

        Map::ObjectList objects = map->getDynamicObjects();

        foreach (Object *p, objects) {
            if (chr = dynamic_cast<Character *>(p)) {
                sendPacketToAll(L, genPacket::addCharacter(
                    chr->getX(), chr->getY(), chr->getZ(), chr->getMobID(), chr->getID(), chr->getRed(), chr->getGreen(), chr->getBlue(), chr->getAlpha()));
            }
        }
        return 0;
    }

    /**
     * Move a character.
     */
    int moveCharacter(lua_State *L)
    {
        unsigned int id = lua_tonumber(L, 1);
        int dir = lua_tonumber(L, 2);

        Map *map = reinterpret_cast<Map *>(Lua::getRegisteredPointer(L, LUA_SR_MAP));
        Character *chr = dynamic_cast<Character *>(map->getDynamicObject(id));
        if (!chr) return 0;

        if(chr->isMoving()) {
            return 0;
        }

        // Get the X and Y location of the character.
        Location loc(chr->getLocation());
        int x = loc.getX();
        int y = loc.getY();
        int z = loc.getZ();
        int h = chr->getHeight();

        // Get the offset.
        int xOffset = 0;
        int yOffset = 0;
        int newZ = 0;
        if (dir == 0 && map->canWalk(x, y, x - 1, y - 1, z, h)) {
            xOffset--;
            yOffset--;
            newZ = map->getBlockedHeight(x - 1, y - 1, z, h);
        } else if (dir == 1 && map->canWalk(x, y, x + 1, y - 1, z, h)) {
            xOffset++;
            yOffset--;
            newZ = map->getBlockedHeight(x + 1, y - 1, z, h);
        } else if (dir == 2 && map->canWalk(x, y, x - 1, y + 1, z, h)) {
            xOffset--;
            yOffset++;
            newZ = map->getBlockedHeight(x - 1, y + 1, z, h);
        } else if (dir == 3 && map->canWalk(x, y, x + 1, y + 1, z, h)) {
            xOffset++;
            yOffset++;
            newZ = map->getBlockedHeight(x + 1, y + 1, z, h);
        } else if (dir == 4 && map->canWalk(x, y, x, y - 1, z, h)) {
            yOffset--;
            newZ = map->getBlockedHeight(x, y - 1, z, h);
        } else if (dir == 5 && map->canWalk(x, y, x - 1, y, z, h)) {
            xOffset--;
            newZ = map->getBlockedHeight(x - 1, y, z, h);
        } else if (dir == 6 && map->canWalk(x, y, x + 1, y, z, h)) {
            xOffset++;
            newZ = map->getBlockedHeight(x + 1, y, z, h);
        } else if (dir == 7 && map->canWalk(x, y, x, y + 1, z, h)) {
            yOffset++;
            newZ = map->getBlockedHeight(x, y + 1, z, h);
        }

        // Handle movement.
        if (xOffset || yOffset) {
            xOffset *= TILE_SIZE;
            yOffset *= TILE_SIZE;
            int zOffset = newZ - loc.getZ();
            chr->move(xOffset, yOffset, zOffset);
            sendPacketToAll(L, genPacket::moveObject(id, xOffset, yOffset, zOffset));
        } else {
            // Face the character appropriately.
            switch (dir) {
                case 0: // up
                case 1:
                case 4:
                    chr->face(0, -1);
                    break;
                case 2: // down
                case 3:
                case 7:
                    chr->face(0, 1);
                    break;
                case 5: // left
                    chr->face(-1, 0);
                    break;
                case 6: // right
                    chr->face(1, 0);
                    break;
            }
        }
    }

    static int localCall(lua_State *L)
    {
        boost::shared_ptr<Packet> pkt = Lua::createRemoteCall(L);
        Lua::processRemoteCall(L, *pkt, -1);
    }

    static int remoteCall(lua_State *L)
    {
        lua_Number num = luaL_checknumber(L, 1);
        lua_remove(L, 1);
        boost::shared_ptr<Packet> pkt = Lua::createRemoteCall(L);
        lua_pushnumber(L, num);
        lua_insert(L, 1);
        sendPacket(L, *pkt);
    }

    /**
     * Stores a list of lua functions.
     */
    static const struct luaL_Reg odysis [] = {
        {"sendLoginOKPacket", sendLoginOKPacket},
        {"sendLoginFailPacket", sendLoginFailPacket},
        {"addCharacter", addCharacter},
        {"setClientCharID", setClientCharID},
        {"getClientCharID", getClientCharID},
        {"moveCharacter", moveCharacter},
        {"localCall", localCall},
        {"remoteCall", remoteCall},
        {NULL, NULL}
    };
}

void call_gotLoginPacket(lua_State *L, double sock, const std::string &username, const std::string &pass)
{
    lua_getglobal(L, "gotLoginPacket");
    lua_pushnumber(L, sock);
    lua_pushstring(L, username.c_str());
    lua_pushstring(L, pass.c_str());
    if (lua_pcall(L, 3, 0, 0) != 0) {
        throw std::runtime_error(lua_tostring(L, -1));
    }
}

void call_gotMoveRequestPacket(lua_State *L, double sock, int direction)
{
    lua_getglobal(L, "gotMoveRequestPacket");
    lua_pushnumber(L, sock);
    lua_pushnumber(L, direction);
    if (lua_pcall(L, 2, 0, 0) != 0) {
        throw std::runtime_error(lua_tostring(L, -1));
    }
}

int openOdysisLib(lua_State *L)
{
    luaL_register(L, "odysis", odysis);
    return 1;
}
